﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Dapper.Sharding
{
    public abstract class ISharding<T> where T : class
    {
        public ISharding(ITable<T>[] list)
        {
            if (list[0].SqlField.IsIdentity)
            {
                throw new Exception("auto increment of primary key is not allowed");
            }
            TableList = list;
            Query = new ShardingQuery<T>(TableList);
            SqlField = list[0].SqlField;
            AllClickHouse = TableList.Any(a => a.DbType != DataBaseType.ClickHouse);
        }

        #region base

        public ITable<T>[] TableList { get; }

        public ShardingQuery<T> Query { get; }

        protected SqlFieldEntity SqlField { get; }

        protected bool AllClickHouse { get; }

        protected void Wrap(DistributedTransaction tran, Action action)
        {
            if (tran == null && !AllClickHouse)
            {
                using var tn = new DistributedTransaction();
                action();
                tn.Commit();
            }
            else
            {
                action();
            }
        }

        protected async Task WrapAsync(DistributedTransaction tran, Action action)
        {
            if (tran == null && !AllClickHouse)
            {
				using var tn = new DistributedTransaction();
                action();
				tn.Commit();
            }
            else
            {
                action();
            }
        }

        protected TResult Wrap<TResult>(DistributedTransaction tran, Func<TResult> func)
        {
            if (tran == null && !AllClickHouse)
            {
                using var tn = new DistributedTransaction();
                var result = func();
                tn.Commit();
                return result;
            }
            return func();
        }

        protected async Task<TResult> WrapAsync<TResult>(DistributedTransaction tran, Func<TResult> func)
        {
            if (tran == null && !AllClickHouse)
            {
				using var tn = new DistributedTransaction();
                var result = func();
				tn.Commit();
                return result;
            }
            return func();
        }

        public abstract ITable<T> GetTableById(object id);

        public abstract Task<ITable<T>> GetTableByIdAsync(object id);

        public abstract ITable<T> GetTableByModel(T model);

        public abstract Task<ITable<T>> GetTableByModelAsync(T model);

        public virtual Dictionary<ITable<T>, List<object>> GetTableByGroupIds(object ids)
        {
            var dict = new Dictionary<ITable<T>, List<object>>();
            var idsList = CommonUtil.GetMultiExec(ids);
            if (idsList != null)
            {
                foreach (var id in idsList)
                {
                    var table = GetTableById(id);
                    if (!dict.ContainsKey(table))
                    {
                        dict.Add(table, new List<object>());
                    }
                    dict[table].Add(id);
                }
            }
            return dict;
        }

        public virtual Dictionary<ITable<T>, List<T>> GetTableByGroupModelList(IEnumerable<T> modelList)
        {
            var dict = new Dictionary<ITable<T>, List<T>>();
            foreach (var item in modelList)
            {
                var table = GetTableByModel(item);
                if (!dict.ContainsKey(table))
                {
                    dict.Add(table, new List<T>());
                }
                dict[table].Add(item);
            }
            return dict;
        }

        #endregion

        #region insert

        public virtual void Insert(T model, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            tb.Insert(model, tran, timeout);
        }

        public virtual void Insert(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.Insert(item.Value, tran, timeout);
                 }
             });
        }

        public virtual void InsertIfNoExists(T model, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            tb.InsertIfNoExists(model, tran, timeout);
        }

        public virtual void InsertIfNoExists(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.InsertIfNoExists(item.Value, tran, timeout);
                 }
             });
        }

        public virtual void Merge(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            tb.Merge(model, fields, tran, timeout);
        }

        public virtual void Merge(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.Merge(item.Value, fields, tran, timeout);
                 }
             });
        }

        public virtual void MergeIgnore(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            tb.MergeIgnore(model, fields, tran, timeout);
        }

        public virtual void MergeIgnore(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.MergeIgnore(item.Value, fields, tran, timeout);
                 }
             });
        }

        #endregion

        #region insertAsync

        public virtual async Task InsertAsync(T model, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            await tb.InsertAsync(model, tran, timeout, cancellationToken);
        }

        public virtual async Task InsertAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.InsertAsync(item.Value, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public virtual async Task InsertIfNoExistsAsync(T model, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            await tb.InsertIfNoExistsAsync(model, tran, timeout, cancellationToken);
        }

        public virtual async Task InsertIfNoExistsAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.InsertIfNoExistsAsync(item.Value, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public virtual async Task MergeAsync(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            await tb.MergeAsync(model, fields, tran, timeout, cancellationToken);
        }

        public virtual async Task MergeAsync(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.MergeAsync(item.Value, fields, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public virtual async Task MergeIgnoreAsync(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            await tb.MergeIgnoreAsync(model, fields, tran, timeout, cancellationToken);
        }

        public virtual async Task MergeIgnoreAsync(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.MergeIgnoreAsync(item.Value, fields, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        #endregion

        #region update

        public virtual int Update(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            return tb.Update(model, fields, tran, timeout);
        }

        public virtual void Update(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.Update(item.Value, fields, tran, timeout);
                 }
             });
        }

        public virtual int UpdateIgnore(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            return tb.UpdateIgnore(model, fields, tran, timeout);
        }

        public virtual void UpdateIgnore(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.UpdateIgnore(item.Value, fields, tran, timeout);
                 }
             });
        }

        public int UpdateByWhere(T model, string where, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
        {
            return Wrap(tran, () =>
             {
                 int count = 0;
                 foreach (var item in TableList)
                 {
                     count += item.UpdateByWhere(model, where, fields, tran, timeout);
                 }
                 return count;
             });
        }


        public int UpdateByWhereIgnore(T model, string where, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
        {
            return Wrap(tran, () =>
             {
                 int count = 0;
                 foreach (var item in TableList)
                 {
                     count += item.UpdateByWhereIgnore(model, where, fields, tran, timeout);
                 }
                 return count;
             });
        }

        #endregion

        #region updateAsync

        public virtual async Task<int> UpdateAsync(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            return await tb.UpdateAsync(model, fields, tran, timeout, cancellationToken);
        }

        public virtual async Task UpdateAsync(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.UpdateAsync(item.Value, fields, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public virtual async Task<int> UpdateIgnoreAsync(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            return await tb.UpdateIgnoreAsync(model, fields, tran, timeout, cancellationToken);
        }

        public virtual async Task UpdateIgnoreAsync(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.UpdateIgnoreAsync(item.Value, fields, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public async Task<int> UpdateByWhereAsync(T model, string where, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                int count = 0;
                foreach (var item in TableList)
                {
                    count += await item.UpdateByWhereAsync(model, where, fields, tran, timeout, cancellationToken);
                }
                return count;
            });
            return await task;
        }


        public async Task<int> UpdateByWhereIgnoreAsync(T model, string where, List<string> fields, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                int count = 0;
                foreach (var item in TableList)
                {
                    count += await item.UpdateByWhereIgnoreAsync(model, where, fields, tran, timeout, cancellationToken);
                }
                return count;
            });
            return await task;
        }

        #endregion

        #region delete

        public virtual int Delete(object id, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableById(id);
            return tb.Delete(id, tran, timeout);
        }

        public virtual int Delete(T model, DistributedTransaction tran = null, int? timeout = null)
        {
            var tb = GetTableByModel(model);
            return tb.Delete(model, tran, timeout);
        }

        public virtual int DeleteByIds(object ids, DistributedTransaction tran = null, int? timeout = null)
        {
            if (CommonUtil.ObjectIsEmpty(ids))
                return 0;
            return Wrap(tran, () =>
             {
                 int count = 0;
                 var dict = GetTableByGroupIds(ids);
                 foreach (var item in dict)
                 {
                     count += item.Key.DeleteByIds(item.Value, tran, timeout);
                 }
                 return count;
             });
        }

        public virtual void Delete(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
        {
            Wrap(tran, () =>
             {
                 var dict = GetTableByGroupModelList(modelList);
                 foreach (var item in dict)
                 {
                     item.Key.Delete(item.Value, tran, timeout);
                 }
             });
        }

        public int DeleteByWhere(string where, object param = null, DistributedTransaction tran = null, int? timeout = null)
        {
            return Wrap(tran, () =>
             {
                 int count = 0;
                 foreach (var item in TableList)
                 {
                     count += item.DeleteByWhere(where, param, tran, timeout);
                 }
                 return count;
             });
        }

        public int DeleteAll(DistributedTransaction tran = null, int? timeout = null)
        {
            return Wrap(tran, () =>
             {
                 int count = 0;
                 foreach (var item in TableList)
                 {
                     count += item.DeleteAll(tran, timeout);
                 }
                 return count;
             });
        }

        #endregion

        #region deleteAsync

        public virtual async Task<int> DeleteAsync(object id, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByIdAsync(id);
            return await tb.DeleteAsync(id, tran, timeout, cancellationToken);
        }

        public virtual async Task<int> DeleteAsync(T model, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var tb = await GetTableByModelAsync(model);
            return await tb.DeleteAsync(model, tran, timeout, cancellationToken);
        }

        public virtual async Task<int> DeleteByIdsAsync(object ids, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            if (CommonUtil.ObjectIsEmpty(ids))
                return 0;
            var task = await WrapAsync(tran, async () =>
            {
                int count = 0;
                var dict = GetTableByGroupIds(ids);
                foreach (var item in dict)
                {
                    count += await item.Key.DeleteByIdsAsync(item.Value, tran, timeout, cancellationToken);
                }
                return count;
            });
            return await task;
        }

        public virtual async Task DeleteAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                var dict = GetTableByGroupModelList(modelList);
                foreach (var item in dict)
                {
                    await item.Key.DeleteAsync(item.Value, tran, timeout, cancellationToken);
                }
            });
            await task;
        }

        public async Task<int> DeleteByWhereAsync(string where, object param = null, DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                int count = 0;
                foreach (var item in TableList)
                {
                    count += await item.DeleteByWhereAsync(where, param, tran, timeout, cancellationToken);
                }
                return count;
            });
            return await task;
        }

        public async Task<int> DeleteAllAsync(DistributedTransaction tran = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var task = await WrapAsync(tran, async () =>
            {
                int count = 0;
                foreach (var item in TableList)
                {
                    count += await item.DeleteAllAsync(tran, timeout, cancellationToken);
                }
                return count;
            });
            return await task;
        }

        #endregion

        #region query

        public virtual async Task<bool> ExistsAsync(object id, CancellationToken cancellationToken = default)
        {
            var table = await GetTableByIdAsync(id);
            return await table.ExistsAsync(id, cancellationToken: cancellationToken);
        }

        public virtual async Task<bool> ExistsAsync(T model, CancellationToken cancellationToken = default)
        {
            var table = await GetTableByModelAsync(model);
            return await table.ExistsAsync(model, cancellationToken: cancellationToken);
        }

        public async Task<bool> ExistsWhereAsync(string where, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            var taskList = TableList.Select(s =>
            {
                return s.ExistsWhereAsync(where, param, timeout: timeout, cancellationToken: cancellationToken);
            });
            var result = await Task.WhenAll(taskList);
            return result.Any(a => a == true);
        }


        public virtual async Task<T> GetByIdAsync(object id, string returnFields = null, CancellationToken cancellationToken = default)
        {
            var table = await GetTableByIdAsync(id);
            return await table.GetByIdAsync(id, returnFields, cancellationToken: cancellationToken);
        }

        public virtual async Task<IEnumerable<T>> GetByIdsAsync(object ids, string returnFields = null, CancellationToken cancellationToken = default)
        {
            if (CommonUtil.ObjectIsEmpty(ids))
                return Enumerable.Empty<T>();
            var dict = GetTableByGroupIds(ids);
            var taskList = dict.Select(s =>
            {
                return s.Key.GetByIdsAsync(s.Value, returnFields, cancellationToken: cancellationToken);
            });
            var result = await Task.WhenAll(taskList);
            return result.ConcatItem();
        }

        public async Task TruncateAsync(CancellationToken cancellationToken = default)
        {
            var taskList = TableList.Select(s =>
            {
                return s.TruncateAsync(cancellationToken);
            });
            await Task.WhenAll(taskList);
        }

        #endregion

    }
}
